% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
%   http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.

-module(couch_auth_cache).


-export([
    get_user_creds/1,
    get_user_creds/2,
    update_user_creds/3,
    get_admin/1,
    add_roles/2,
    auth_design_doc/1,
    ensure_users_db_exists/0
]).


-include_lib("couch/include/couch_db.hrl").
-include_lib("couch/include/couch_js_functions.hrl").


-spec get_user_creds(UserName::string() | binary()) ->
    {ok, Credentials::list(), term()} | nil.

get_user_creds(UserName) ->
    get_user_creds(nil, UserName).

-spec get_user_creds(Req::#httpd{} | nil, UserName::string() | binary()) ->
    {ok, Credentials::list(), term()} | nil.

get_user_creds(Req, UserName) when is_list(UserName) ->
    get_user_creds(Req, ?l2b(UserName));

get_user_creds(_Req, UserName) ->
    UserCreds = case get_admin(UserName) of
    nil ->
        get_from_db(UserName);
    Props ->
        case get_from_db(UserName) of
        nil ->
            Props;
        UserProps when is_list(UserProps) ->
            add_roles(Props, couch_util:get_value(<<"roles">>, UserProps))
        end
    end,
    validate_user_creds(UserCreds).

update_user_creds(_Req, UserDoc, _AuthCtx) ->
    ok = ensure_users_db_exists(),
    couch_util:with_db(users_db(), fun(UserDb) ->
        {ok, _NewRev} = couch_db:update_doc(UserDb, UserDoc, []),
        ok
    end).

add_roles(Props, ExtraRoles) ->
    CurrentRoles = couch_util:get_value(<<"roles">>, Props),
    lists:keyreplace(<<"roles">>, 1, Props, {<<"roles">>, CurrentRoles ++ ExtraRoles}).

get_admin(UserName) when is_binary(UserName) ->
    get_admin(?b2l(UserName));
get_admin(UserName) when is_list(UserName) ->
    case config:get("admins", UserName) of
    "-hashed-" ++ HashedPwdAndSalt ->
        % the name is an admin, now check to see if there is a user doc
        % which has a matching name, salt, and password_sha
        [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
        make_admin_doc(HashedPwd, Salt);
    "-pbkdf2-" ++ HashedPwdSaltAndIterations ->
        [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
        make_admin_doc(HashedPwd, Salt, Iterations);
    _Else ->
	nil
    end.

make_admin_doc(HashedPwd, Salt) ->
    [{<<"roles">>, [<<"_admin">>]},
     {<<"salt">>, ?l2b(Salt)},
     {<<"password_scheme">>, <<"simple">>},
     {<<"password_sha">>, ?l2b(HashedPwd)}].

make_admin_doc(DerivedKey, Salt, Iterations) ->
    [{<<"roles">>, [<<"_admin">>]},
     {<<"salt">>, ?l2b(Salt)},
     {<<"iterations">>, list_to_integer(Iterations)},
     {<<"password_scheme">>, <<"pbkdf2">>},
     {<<"derived_key">>, ?l2b(DerivedKey)}].


get_from_db(UserName) ->
    ok = ensure_users_db_exists(),
    couch_util:with_db(users_db(), fun(Db) ->
        DocId = <<"org.couchdb.user:", UserName/binary>>,
        try
            {ok, Doc} = couch_db:open_doc(Db, DocId, [conflicts]),
            {DocProps} = couch_doc:to_json_obj(Doc, []),
            DocProps
        catch
        _:_Error ->
            nil
        end
    end).


validate_user_creds(nil) ->
    nil;
validate_user_creds(UserCreds) ->
    case couch_util:get_value(<<"_conflicts">>, UserCreds) of
    undefined ->
        ok;
    _ConflictList ->
        throw({unauthorized,
            <<"User document conflicts must be resolved before the document",
              " is used for authentication purposes.">>
        })
    end,
    {ok, UserCreds, nil}.


users_db() ->
    DbNameList = config:get("couch_httpd_auth", "authentication_db", "_users"),
    ?l2b(DbNameList).


ensure_users_db_exists() ->
    Options = [?ADMIN_CTX, nologifmissing],
    case couch_db:open(users_db(), Options) of
    {ok, Db} ->
        ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
        couch_db:close(Db);
    _Error ->
        {ok, Db} = couch_db:create(users_db(), Options),
        ok = ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
        couch_db:close(Db)
    end,
    ok.


ensure_auth_ddoc_exists(Db, DDocId) ->
    case couch_db:open_doc(Db, DDocId) of
    {not_found, _Reason} ->
        {ok, AuthDesign} = auth_design_doc(DDocId),
        {ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []);
    {ok, Doc} ->
        {Props} = couch_doc:to_json_obj(Doc, []),
        case couch_util:get_value(<<"validate_doc_update">>, Props, []) of
            ?AUTH_DB_DOC_VALIDATE_FUNCTION ->
                ok;
            _ ->
                Props1 = lists:keyreplace(<<"validate_doc_update">>, 1, Props,
                    {<<"validate_doc_update">>,
                    ?AUTH_DB_DOC_VALIDATE_FUNCTION}),
                couch_db:update_doc(Db, couch_doc:from_json_obj({Props1}), [])
        end
    end,
    ok.

auth_design_doc(DocId) ->
    DocProps = [
        {<<"_id">>, DocId},
        {<<"language">>,<<"javascript">>},
        {<<"validate_doc_update">>, ?AUTH_DB_DOC_VALIDATE_FUNCTION}
    ],
    {ok, couch_doc:from_json_obj({DocProps})}.
